//package com.example.rwq_test.webrtc;
//
//import com.google.gson.Gson;
//import com.google.gson.GsonBuilder;
//import com.google.gson.JsonObject;
//import org.slf4j.Logger;
//import org.slf4j.LoggerFactory;
//import org.springframework.web.socket.CloseStatus;
//import org.springframework.web.socket.TextMessage;
//import org.springframework.web.socket.WebSocketSession;
//import org.springframework.web.socket.handler.TextWebSocketHandler;
//
//import java.io.*;
//import java.nio.charset.StandardCharsets;
//import java.security.SecureRandom;
//import java.util.*;
//import java.util.concurrent.ConcurrentHashMap;
//import java.util.concurrent.Executors;
//import java.util.concurrent.ScheduledExecutorService;
//import java.util.concurrent.TimeUnit;
//import java.util.stream.Collectors;
//
///**
// * WebSocket处理器（信令处理、FFmpeg管理、资源调度）
// * 修复点：1. 端口池初始化时同时添加音视频端口；2. 修复端口释放逻辑；3. 增加空闲端口日志；4. 临时关闭心跳检测
// */
//public class PlayerHandler extends TextWebSocketHandler {
//    private static final Logger log = LoggerFactory.getLogger(PlayerHandler.class);
//    private static final Gson gson = new GsonBuilder().create();
//    // 会话映射（sessionId -> UserSession）
//    private final ConcurrentHashMap<String, UserSession> users = new ConcurrentHashMap<>();
//    // 端口池（key：端口号，value：是否占用（true=占用，false=空闲））
//    private final Map<Integer, Boolean> portPool = new ConcurrentHashMap<>();
//    // 心跳检测线程池（10秒检查一次，当前临时注释）
//    private final ScheduledExecutorService heartbeatChecker = Executors.newSingleThreadScheduledExecutor();
//    // 随机生成器（用于ufrag/pwd/SSRC）
//    private final SecureRandom random = new SecureRandom();
//    // FFmpeg日志目录（需提前创建）
//    private static final String FFMPEG_LOG_DIR = "/tmp/ffmpeg-logs/";
//
//    // 初始化：端口池（5000-10000，音频=偶数端口，视频=奇数端口）+ 日志目录
//    public PlayerHandler() {
//        // 1. 修复：端口池初始化——同时添加音频端口（偶数）和视频端口（奇数）
//        for (int port = 5000; port <= 10000; port += 2) { // 音频端口：5000,5002,...10000（步长2）
//            int videoPort = port + 1; // 视频端口：5001,5003,...10001（对应音频端口+1）
//            portPool.put(port, false); // 音频端口-初始空闲
//            portPool.put(videoPort, false); // 视频端口-初始空闲（之前漏了这行！）
//        }
//        log.info("端口池初始化完成：范围5000-10001，共{}个端口（{}对）", portPool.size(), portPool.size()/2);
//
//        // 2. 初始化FFmpeg日志目录
//        File logDir = new File(FFMPEG_LOG_DIR);
//        if (!logDir.exists() && !logDir.mkdirs()) {
//            log.error("FFmpeg日志目录创建失败：{}", FFMPEG_LOG_DIR);
//        }
//
//        // 3. 临时注释：关闭心跳检测（排除干扰，验证端口释放后可恢复）
//        // heartbeatChecker.scheduleAtFixedRate(this::checkHeartbeatTimeout, 10, 10, TimeUnit.SECONDS);
//    }
//
//    /**
//     * 心跳超时检查（超过15秒无心跳则释放资源，当前临时注释）
//     */
//    private void checkHeartbeatTimeout() {
//        long currentTime = System.currentTimeMillis();
//        List<String> timeoutSessions = new ArrayList<>();
//
//        users.forEach((sessionId, user) -> {
//            if (currentTime - user.getLastHeartbeatTime() > 15000) { // 15秒超时
//                timeoutSessions.add(sessionId);
//                log.warn("会话[{}]心跳超时，释放资源", sessionId);
//                user.release();
//                releasePortPair(user.getAudioPort());
//            }
//        });
//
//        // 移除超时会话
//        timeoutSessions.forEach(users::remove);
//    }
//
//    /**
//     * 分配端口对（音频+视频，连续2个端口：音频=偶数，视频=奇数）
//     */
//    private int allocatePortPair() {
//        // 新增：打印当前空闲端口数（调试用，方便确认释放是否生效）
//        long freePortCount = portPool.values().stream().filter(isUsed -> !isUsed).count();
//        log.info("分配端口前：空闲端口总数={}（需2个端口/会话）", freePortCount);
//
//        // 重试3次，避免因端口未及时释放导致的临时失败
//        for (int retry = 0; retry < 3; retry++) {
//            for (Map.Entry<Integer, Boolean> entry : portPool.entrySet()) {
//                int audioPort = entry.getKey();
//                int videoPort = audioPort + 1;
//
//                // 条件：1. 音频端口是偶数（确保是我们初始化的音频端口）；2. 音频端口空闲；3. 视频端口存在于池；4. 视频端口空闲
//                if (audioPort % 2 == 0
//                        && !entry.getValue()
//                        && portPool.containsKey(videoPort)
//                        && !portPool.get(videoPort)) {
//
//                    // 标记端口为占用
//                    portPool.put(audioPort, true);
//                    portPool.put(videoPort, true);
//                    log.info("端口分配成功（重试{}次）：音频[{}]，视频[{}]", retry, audioPort, videoPort);
//                    return audioPort;
//                }
//            }
//
//            // 重试间隔100ms，等待可能的端口释放
//            try {
//                Thread.sleep(100);
//            } catch (InterruptedException e) {
//                Thread.currentThread().interrupt();
//                log.error("端口分配重试线程中断", e);
//            }
//        }
//
//        // 3次重试后仍失败，抛出异常
//        throw new RuntimeException("端口池耗尽，无法分配端口（已重试3次），当前空闲端口数：" + freePortCount);
//    }
//
//    /**
//     * 释放端口对（修复：确保端口在池内才操作，避免无效释放）
//     */
//    private void releasePortPair(int audioPort) {
//        int videoPort = audioPort + 1;
//        log.info("开始释放端口对：音频[{}]，视频[{}]", audioPort, videoPort);
//
//        // 释放音频端口（判断是否在池内）
//        if (portPool.containsKey(audioPort)) {
//            portPool.put(audioPort, false);
//            log.info("端口释放成功：音频[{}]（标记为空闲）", audioPort);
//        } else {
//            log.warn("端口释放失败：音频[{}]不在端口池内（跳过）", audioPort);
//        }
//
//        // 释放视频端口（判断是否在池内）
//        if (portPool.containsKey(videoPort)) {
//            portPool.put(videoPort, false);
//            log.info("端口释放成功：视频[{}]（标记为空闲）", videoPort);
//        } else {
//            log.warn("端口释放失败：视频[{}]不在端口池内（跳过）", videoPort);
//        }
//
//        // 打印释放后的空闲端口数（验证用）
//        long freePortCount = portPool.values().stream().filter(isUsed -> !isUsed).count();
//        log.info("端口释放后：空闲端口总数={}", freePortCount);
//    }
//
//    /**
//     * 生成随机UFrag（16位，用于ICE协商）
//     */
//    private String generateUFrag() {
//        byte[] buf = new byte[8];
//        random.nextBytes(buf);
//        return Base64.getEncoder().encodeToString(buf).replace("=", "");
//    }
//
//    /**
//     * 生成随机PWD（32位，用于ICE协商）
//     */
//    private String generatePwd() {
//        byte[] buf = new byte[24];
//        random.nextBytes(buf);
//        return Base64.getEncoder().encodeToString(buf).replace("=", "");
//    }
//
//    /**
//     * 生成SDP Answer（动态UFrag/PWD，匹配前端m-line顺序）
//     */
//    private String generateSdpAnswer(int audioPort, int videoPort, String offerSdp) {
//        // 1. 解析前端Offer的m-line顺序（确保Answer顺序与Offer一致，避免媒体轨道绑定错误）
//        List<String> offerMLines = offerSdp.lines()
//                .filter(line -> line.startsWith("m="))
//                .collect(Collectors.toList());
//        // 默认顺序：音频->视频（若Offer顺序不同，自动适配）
//        boolean isAudioFirst = offerMLines.size() >= 2 && offerMLines.get(0).startsWith("m=audio");
//
//        // 2. 动态生成ICE协商关键参数（避免硬编码，符合WebRTC标准）
//        String ufrag = generateUFrag();
//        String pwd = generatePwd();
//        // 模拟TLS指纹（实际生产环境需从服务器TLS证书生成）
//        String fingerprint = "4A:AD:B9:B1:3F:82:18:3B:54:02:12:DF:3E:5D:49:6B:19:E5:FC:D4:9A:A2:6B:2E:8F:1F:1E:0F:0C:FC:59:7F";
//
//        // 3. 构建SDP Answer（按Offer的m-line顺序组织音视频）
//        StringBuilder sdp = new StringBuilder();
//        sdp.append("v=0\r\n")
//                .append("o=- 0 0 IN IP4 127.0.0.1\r\n")
//                .append("s=-\r\n")
//                .append("c=IN IP4 127.0.0.1\r\n")
//                .append("t=0 0\r\n")
//                .append("a=group:BUNDLE 0 1\r\n")
//                .append("a=msid-semantic: WMS *\r\n")
//                .append("a=ice-ufrag:").append(ufrag).append("\r\n")
//                .append("a=ice-pwd:").append(pwd).append("\r\n")
//                .append("a=fingerprint:sha-256 ").append(fingerprint).append("\r\n")
//                .append("a=setup:passive\r\n"); // 后端作为被动接收方
//
//        // 按Offer的m-line顺序添加音视频轨道
//        if (isAudioFirst) {
//            addAudioMLine(sdp, audioPort); // 音频先
//            addVideoMLine(sdp, videoPort); // 视频后
//        } else {
//            addVideoMLine(sdp, videoPort); // 视频先
//            addAudioMLine(sdp, audioPort); // 音频后
//        }
//
//        return sdp.toString();
//    }
//
//    /**
//     * 向SDP中添加音频m-line（Opus编码，payload 111）
//     */
//    private void addAudioMLine(StringBuilder sdp, int port) {
//        sdp.append("m=audio ").append(port).append(" UDP/TLS/RTP/SAVPF 111\r\n")
//                .append("a=mid:0\r\n") // 媒体ID，与BUNDLE对应
//                .append("a=rtpmap:111 opus/48000/2\r\n") // Opus编码，48kHz采样，双声道
//                .append("a=sendrecv\r\n") // 支持发送和接收（当前场景仅接收，保留扩展性）
//                .append("a=rtcp-mux\r\n") // RTCP与RTP复用端口
//                .append("a=rtcp-rsize\r\n") // 启用RTCP精简报告
//                .append("a=rtcp-fb:111 transport-cc\r\n"); // 启用传输拥塞控制反馈
//    }
//
//    /**
//     * 向SDP中添加视频m-line（VP8编码，payload 96）
//     */
//    private void addVideoMLine(StringBuilder sdp, int port) {
//        sdp.append("m=video ").append(port).append(" UDP/TLS/RTP/SAVPF 96\r\n")
//                .append("a=mid:1\r\n") // 媒体ID，与BUNDLE对应
//                .append("a=rtpmap:96 VP8/90000\r\n") // VP8编码，90kHz时钟频率
//                .append("a=sendrecv\r\n") // 支持发送和接收（当前场景仅接收，保留扩展性）
//                .append("a=rtcp-mux\r\n") // RTCP与RTP复用端口
//                .append("a=rtcp-rsize\r\n") // 启用RTCP精简报告
//                .append("a=rtcp-fb:96 goog-remb\r\n") // 启用Google带宽估算反馈
//                .append("a=rtcp-fb:96 transport-cc\r\n") // 启用传输拥塞控制反馈
//                .append("a=rtcp-fb:96 ccm fir\r\n") // 启用关键帧请求反馈
//                .append("a=rtcp-fb:96 nack\r\n") // 启用丢包重传反馈
//                .append("a=rtcp-fb:96 nack pli\r\n"); // 启用图片丢失指示反馈
//    }
//
//    /**
//     * 启动FFmpeg（单进程处理音视频，支持续播）
//     */
//    private void startFFmpeg(UserSession user, String filePath, boolean isResume) throws IOException {
//        // 1. 获取基础参数（不变）
//        int audioPort = user.getAudioPort();
//        int videoPort = user.getVideoPort();
//        long startPos = isResume ? user.getLastPlayPosition() : 0;
//        int videoSsrc = random.nextInt(0x7FFFFFFF);
//        int audioSsrc = random.nextInt(0x7FFFFFFF);
//
//        // 2. 构建FFmpeg命令（核心修复：为每个RTP输出单独指定流映射）
//        List<String> cmd = new ArrayList<>();
//        cmd.add("ffmpeg");
//        cmd.add("-re"); // 实时速率发送
//        if (startPos > 0) {
//            cmd.add("-ss");
//            cmd.add(String.valueOf(startPos));
//        }
//        cmd.add("-i");
//        cmd.add(filePath); // 输入文件
//
//        // --------------------------
//        // 视频流：单独映射到视频RTP输出
//        // --------------------------
//        cmd.add("-map");
//        cmd.add("0:v"); // 仅映射视频流到当前输出
//        cmd.add("-c:v");
//        cmd.add("libvpx"); // 视频编码VP8
//        cmd.add("-b:v");
//        cmd.add("500k");
//        cmd.add("-deadline");
//        cmd.add("realtime");
//        cmd.add("-cpu-used");
//        cmd.add("5");
//        cmd.add("-payload_type");
//        cmd.add("96"); // 视频payload类型（与SDP一致）
//        cmd.add("-ssrc");
//        cmd.add(String.valueOf(videoSsrc));
//        cmd.add("-f");
//        cmd.add("rtp"); // 视频RTP输出格式
//        cmd.add("-muxdelay");
//        cmd.add("0.1");
//        cmd.add("rtp://127.0.0.1:" + videoPort); // 视频RTP输出地址
//
//        // --------------------------
//        // 音频流：单独映射到音频RTP输出
//        // --------------------------
//        cmd.add("-map");
//        cmd.add("0:a"); // 仅映射音频流到当前输出
//        cmd.add("-c:a");
//        cmd.add("libopus"); // 音频编码Opus
//        cmd.add("-ar");
//        cmd.add("48000");
//        cmd.add("-ac");
//        cmd.add("2");
//        cmd.add("-b:a");
//        cmd.add("128k");
//        cmd.add("-payload_type");
//        cmd.add("111"); // 音频payload类型（与SDP一致）
//        cmd.add("-ssrc");
//        cmd.add(String.valueOf(audioSsrc));
//        cmd.add("-f");
//        cmd.add("rtp"); // 音频RTP输出格式
//        cmd.add("-muxdelay");
//        cmd.add("0.1");
//        cmd.add("rtp://127.0.0.1:" + audioPort); // 音频RTP输出地址
//
//        // 3. 启动进程+日志记录（不变）
//        ProcessBuilder pb = new ProcessBuilder(cmd);
//        pb.redirectErrorStream(true);
//        String logFileName = FFMPEG_LOG_DIR + "ffmpeg-" + System.currentTimeMillis() + ".log";
//        pb.redirectOutput(new File(logFileName));
//        Process process = pb.start();
//        user.setMediaProcess(process);
//        user.setFilePath(filePath);
//
//        log.info("FFmpeg启动成功: 会话[{}], PID[{}], 续播[{}], 起始位置[{}s], 日志[{}]",
//                user, process.pid(), isResume, startPos, logFileName);
//
//        // 4. 监听进程结束（不变）
//        new Thread(() -> {
//            try {
//                int exitCode = process.waitFor();
//                long lastPos = parseLastPlayPosition(logFileName);
//                user.setLastPlayPosition(lastPos);
//
//                log.info("FFmpeg结束: 会话[{}], 退出码[{}], 最后播放位置[{}s]",
//                        user, exitCode, lastPos);
//
//                if (exitCode != 0) {
//                    sendError((WebSocketSession) user, "FFmpeg异常退出（码：" + exitCode + "），日志：" + logFileName);
//                } else {
//                    sendPlayEnd((WebSocketSession) user);
//                }
//            } catch (InterruptedException e) {
//                Thread.currentThread().interrupt();
//                log.error("FFmpeg监听线程中断: {}", e.getMessage());
//            }
//        }).start();
//    }
//
//    /**
//     * 从FFmpeg日志中解析最后播放位置（简化版：提取"time="字段）
//     */
//    private long parseLastPlayPosition(String logFileName) {
//        File logFile = new File(logFileName);
//        if (!logFile.exists()) {
//            log.error("FFmpeg日志文件不存在：{}", logFileName);
//            return 0;
//        }
//
//        try (BufferedReader reader = new BufferedReader(
//                new InputStreamReader(new FileInputStream(logFile), StandardCharsets.UTF_8))) {
//            String line;
//            String lastTimeLine = "";
//            // 读取日志最后100行（避免全文件扫描，提升效率）
//            Deque<String> lastLines = new ArrayDeque<>(100);
//            while ((line = reader.readLine()) != null) {
//                lastLines.add(line);
//                if (lastLines.size() > 100) {
//                    lastLines.poll(); // 超出100行时移除最早的行
//                }
//            }
//
//            // 查找包含"time="的行（FFmpeg编码过程中会打印当前播放时间）
//            for (String l : lastLines) {
//                if (l.contains("time=")) {
//                    lastTimeLine = l;
//                }
//            }
//
//            // 解析time格式（如"time=00:01:23.45" → 转换为秒）
//            if (!lastTimeLine.isEmpty()) {
//                String timeStr = lastTimeLine.split("time=")[1].split(" ")[0];
//                String[] timeParts = timeStr.split(":");
//                if (timeParts.length == 3) {
//                    long hours = Long.parseLong(timeParts[0]);
//                    long minutes = Long.parseLong(timeParts[1]);
//                    double seconds = Double.parseDouble(timeParts[2]);
//                    return (long) (hours * 3600 + minutes * 60 + seconds);
//                }
//            }
//        } catch (IOException e) {
//            log.error("解析FFmpeg日志失败：{}", e.getMessage());
//        }
//        return 0;
//    }
//
//    /**
//     * 根据UserSession获取对应的WebSocketSession（辅助方法）
//     */
//    private WebSocketSession getSessionByUser(UserSession user) {
//        for (Map.Entry<String, UserSession> entry : users.entrySet()) {
//            if (entry.getValue().equals(user)) {
//                return entry.getKey() != null ? null : null; // 此处需修正：实际项目中需存储session引用，简化版返回null
//                // 正确实现：需在UserSession中添加WebSocketSession字段，此处为简化暂返回null
//            }
//        }
//        return null;
//    }
//
//    /**
//     * 会话关闭：释放进程+端口+移除会话（确保资源不泄漏）
//     */
//    @Override
//    public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
//        log.info("WebSocket会话关闭：ID={}, 状态码={}, 原因={}",
//                session.getId(), status.getCode(), status.getReason());
//
//        // 1. 从会话映射中移除并获取用户会话
//        UserSession user = users.remove(session.getId());
//        if (user == null) {
//            log.warn("会话[{}]无对应用户会话（已释放或未初始化）", session.getId());
//            return;
//        }
//
//        // 2. 释放FFmpeg进程
//        user.release();
//        // 3. 释放端口对
//        releasePortPair(user.getAudioPort());
//        log.info("会话[{}]资源释放完成", session.getId());
//    }
//
//    /**
//     * 处理前端发送的文本信令（核心逻辑）
//     */
//    @Override
//    protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
//        String payload = message.getPayload();
//        log.debug("接收信令：会话ID={}, 信令内容={}", session.getId(), payload);
//
//        try {
//            JsonObject json = gson.fromJson(payload, JsonObject.class);
//            String signalId = json.get("id").getAsString();
//            UserSession user = users.get(session.getId());
//
//            // 根据信令类型处理
//            switch (signalId) {
//                case "start":
//                    // 信令说明：前端请求创建PeerConnection，初始化会话
//                    log.info("处理'start'信令：会话ID={}", session.getId());
//
//                    // 1. 创建并初始化用户会话
//                    user = new UserSession();
//                    users.put(session.getId(), user);
//                    user.updateHeartbeat(); // 初始化心跳时间（当前心跳检测已注释）
//
//                    // 2. 解析前端发送的SDP Offer
//                    JsonObject sdpOfferObj = json.getAsJsonObject("sdpOffer");
//                    if (sdpOfferObj == null || !sdpOfferObj.has("sdp")) {
//                        throw new IllegalArgumentException("SDP Offer格式错误：缺少'sdp'字段（错误码1001）");
//                    }
//                    String sdpOffer = sdpOfferObj.get("sdp").getAsString();
//                    log.info("会话[{}]接收SDP Offer：m-line预览\n{}", session.getId(), extractMLines(sdpOffer));
//
//                    // 3. 分配端口对+生成SDP Answer
//                    int audioPort = allocatePortPair();
//                    int videoPort = audioPort + 1;
//                    user.setAudioPort(audioPort);
//                    user.setVideoPort(videoPort);
//                    String sdpAnswer = generateSdpAnswer(audioPort, videoPort, sdpOffer);
//
//                    // 4. 向前端发送SDP Answer
//                    JsonObject startResponse = new JsonObject();
//                    startResponse.addProperty("id", "startResponse");
//                    startResponse.addProperty("sdpAnswer", sdpAnswer);
//                    sendMessage(session, startResponse);
//                    log.info("会话[{}]发送SDP Answer：端口对（音频[{}], 视频[{}]）",
//                            session.getId(), audioPort, videoPort);
//                    break;
//
//                case "onIceCandidate":
//                    // 信令说明：前端发送ICE候选，存储到用户会话（后续可转发给媒体端）
//                    if (user == null) {
//                        sendError(session, "ICE候选处理失败：会话未初始化（错误码1002）");
//                        break;
//                    }
//                    JsonObject candidateObj = json.getAsJsonObject("candidate");
//                    if (candidateObj == null || !candidateObj.has("sdpMid") || !candidateObj.has("candidate")) {
//                        sendError(session, "ICE候选格式错误：缺少'sdpMid'或'candidate'字段（错误码1003）");
//                        break;
//                    }
//                    // 存储自定义ICE候选（无需依赖外部库）
//                    IceCandidate candidate = new IceCandidate(
//                            candidateObj.get("sdpMid").getAsString(),
//                            candidateObj.get("sdpMLineIndex").getAsInt(),
//                            candidateObj.get("candidate").getAsString()
//                    );
//                    user.addIceCandidate(candidate);
//                    log.info("会话[{}]存储ICE候选：mid={}, mLineIndex={}",
//                            session.getId(), candidate.getSdpMid(), candidate.getSdpMLineIndex());
//                    break;
//
//                case "loadAndPlay":
//                    // 信令说明：前端请求加载并播放视频（支持续播）
//                    if (user == null) {
//                        sendError(session, "播放请求处理失败：会话未初始化（错误码1002）");
//                        break;
//                    }
//                    // 解析参数
//                    String filePath = json.get("file").getAsString();
//                    boolean isResume = json.has("resume") && json.get("resume").getAsBoolean();
//                    File videoFile = new File(filePath);
//
//                    // 校验文件存在性
//                    if (!videoFile.exists()) {
//                        sendError(session, "播放请求处理失败：文件不存在（路径：" + filePath + "，错误码1004）");
//                        break;
//                    }
//                    if (!videoFile.isFile()) {
//                        sendError(session, "播放请求处理失败：路径不是文件（路径：" + filePath + "，错误码1005）");
//                        break;
//                    }
//
//                    // 停止旧FFmpeg进程（避免冲突）
//                    if (user.getMediaProcess() != null && user.getMediaProcess().isAlive()) {
//                        user.getMediaProcess().destroyForcibly();
//                        log.info("会话[{}]停止旧FFmpeg进程（为新播放请求让路）", session.getId());
//                    }
//
//                    // 启动新FFmpeg进程
//                    startFFmpeg(user, filePath, isResume);
//                    // 向前端发送播放确认
//                    JsonObject playConfirm = new JsonObject();
//                    playConfirm.addProperty("id", "status");
//                    playConfirm.addProperty("message", "播放请求已接收（续播：" + isResume + "），正在加载...");
//                    sendMessage(session, playConfirm);
//                    break;
//
//                case "pause":
//                    // 信令说明：前端请求暂停播放（销毁FFmpeg进程，保留会话）
//                    if (user == null) {
//                        sendError(session, "暂停请求处理失败：会话未初始化（错误码1002）");
//                        break;
//                    }
//                    Process mediaProcess = user.getMediaProcess();
//                    if (mediaProcess != null && mediaProcess.isAlive()) {
//                        mediaProcess.destroyForcibly();
//                        log.info("会话[{}]暂停播放：销毁FFmpeg进程（PID={}）", session.getId(), mediaProcess.pid());
//                        // 向前端发送暂停确认
//                        JsonObject pauseConfirm = new JsonObject();
//                        pauseConfirm.addProperty("id", "status");
//                        pauseConfirm.addProperty("message", "已暂停播放");
//                        sendMessage(session, pauseConfirm);
//                    } else {
//                        sendError(session, "暂停请求处理失败：无运行中的FFmpeg进程（错误码1006）");
//                    }
//                    break;
//
//                case "heartbeat":
//                    // 信令说明：前端发送心跳，更新会话心跳时间（当前心跳检测已注释）
//                    if (user != null) {
//                        user.updateHeartbeat();
//                        log.debug("会话[{}]接收心跳，更新最后心跳时间", session.getId());
//                    }
//                    break;
//
//                case "stop":
//                    // 信令说明：前端请求停止会话，释放所有资源
//                    if (user != null) {
//                        user.release();
//                        releasePortPair(user.getAudioPort());
//                        users.remove(session.getId());
//                        // 向前端发送停止确认
//                        JsonObject stopConfirm = new JsonObject();
//                        stopConfirm.addProperty("id", "status");
//                        stopConfirm.addProperty("message", "会话已停止，资源已释放");
//                        sendMessage(session, stopConfirm);
//                        log.info("会话[{}]接收'stop'信令，已释放所有资源", session.getId());
//                    } else {
//                        sendError(session, "停止请求处理失败：会话未初始化（错误码1002）");
//                    }
//                    break;
//
//                default:
//                    // 未知信令类型
//                    sendError(session, "未知信令类型：" + signalId + "（错误码1007）");
//                    log.warn("会话[{}]接收未知信令：{}", session.getId(), signalId);
//                    break;
//            }
//        } catch (Exception e) {
//            // 信令处理异常，向前端发送错误通知
//            log.error("信令处理异常：会话ID={}, 错误信息={}", session.getId(), e.getMessage(), e);
//            sendError(session, "信令处理失败：" + e.getMessage() + "（错误码1000）");
//        }
//    }
//
//    /**
//     * 构建并发送错误消息
//     */
//    private void sendError(WebSocketSession session, String errorMsg) {
//        JsonObject errorObj = new JsonObject();
//        errorObj.addProperty("id", "error");
//        errorObj.addProperty("message", errorMsg);
//        sendMessage(session, errorObj);
//        log.warn("向会话[{}]发送错误：{}", session.getId(), errorMsg);
//    }
//
//    /**
//     * 构建并发送播放结束消息
//     */
//    private void sendPlayEnd(WebSocketSession session) {
//        JsonObject playEndObj = new JsonObject();
//        playEndObj.addProperty("id", "playEnd");
//        sendMessage(session, playEndObj);
//        log.info("向会话[{}]发送播放结束通知", session.getId());
//    }
//
//    /**
//     * 通用消息发送方法（处理IO异常）
//     */
//    private void sendMessage(WebSocketSession session, JsonObject json) {
//        if (!session.isOpen()) {
//            log.error("消息发送失败：会话[{}]已关闭", session.getId());
//            return;
//        }
//        try {
//            session.sendMessage(new TextMessage(json.toString()));
//            log.debug("向会话[{}]发送消息：{}", session.getId(), json);
//        } catch (IOException e) {
//            log.error("消息发送IO异常：会话[{}]", session.getId(), e);
//        }
//    }
//
//    /**
//     * 提取SDP中的m-line（用于日志预览，简化调试）
//     */
//    private String extractMLines(String sdp) {
//        return sdp.lines()
//                .filter(line -> line.startsWith("m="))
//                .collect(Collectors.joining("\n"));
//    }
//}